Skip to content

Conversation

@nbaju1
Copy link
Contributor

@nbaju1 nbaju1 commented Sep 22, 2025

Description

Many Azure integrations target a custom API rather than Microsoft Graph. The existing provider always validates tokens via Graph and forwards the RFC 8707 resource parameter, which v2.0 endpoints ignore. This change adds first-class support for custom API audiences and aligns authorization requests with Azure v2.0 semantics.

  • New options: audience (your API’s Application ID URI) and api_client_id (the API app’s GUID used as aud). When audience is set, the provider uses JWTVerifier with the tenant-scoped issuer and JWKS (/v2.0 issuer, /discovery/v2.0/keys) and validates aud against api_client_id. It also validates configuration: api_client_id is required when audience is set, and required_scopes must not be prefixed with the audience. The authorization flow drops the unsupported resource parameter and prefixes non-OpenID scopes with <audience>/, always ensuring openid is included. Microsoft Graph behavior remains the default and unchanged when audience is not provided.
from fastmcp.server.auth.providers.azure import AzureProvider

# Custom API audience (tokens validated via JWKS + issuer)
auth = AzureProvider(
    client_id="your-client-id",
    client_secret="your-client-secret",
    tenant_id="your-tenant-id",
    audience="api://your-api-id",
    api_client_id="00000000-0000-0000-0000-000000000000",
    required_scopes=["read", "write"],  # sent as: openid api://your-api-id/read api://your-api-id/write
    base_url="http://localhost:8000",
)

Backwards compatibility: existing Graph-based setups continue to use Graph verification and the previous default scopes. Tests cover the new JWT path (issuer, JWKS, audience, default scopes) and the updated authorize behavior (resource removal and scope prefixing).

Contributors Checklist

Review Checklist

  • I have self-reviewed my changes
  • My Pull Request is ready for review

Claude Opus 4.1 and GPT-5 used in generating code and PR description

Closes #1663
Closes #1846
Closes #1925

@marvin-context-protocol marvin-context-protocol bot added bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality. auth Related to authentication (Bearer, JWT, OAuth, WorkOS) for client or server. labels Sep 22, 2025
@JonasKs
Copy link

JonasKs commented Sep 22, 2025

Backwards compatibility: existing Graph-based setups continue to use Graph verification and the previous default scopes.

I'd prefer this to be removed, as the current implementation

  1. uses external requests to graph instead of validating the tokens signature
  2. is flawed. The MCP server is not graph, so it shouldn't accept a graph token.

I'll read through code tomorrow.

@nbaju1
Copy link
Contributor Author

nbaju1 commented Sep 23, 2025

Backwards compatibility: existing Graph-based setups continue to use Graph verification and the previous default scopes.

I'd prefer this to be removed, as the current implementation

  1. uses external requests to graph instead of validating the tokens signature
  2. is flawed. The MCP server is not graph, so it shouldn't accept a graph token.

I'll read through code tomorrow.

Agree, just wasn't sure if removing it was too much of a breaking change.

@jlowin
Copy link
Owner

jlowin commented Sep 23, 2025

We've explicitly excluded the auth modules from our breaking change policy for exactly this reason, so I am totally comfortable with the breaking change if it is one. @JonasKs @nbaju1 happy to defer to your thoughts here.

@nbaju1
Copy link
Contributor Author

nbaju1 commented Sep 24, 2025

I've updated the PR to remove validating against Graph entirely and only validate against the MCP server app it self. Want me to update the PR description to reflect this?

I believe the identifier_uri and custom scope will become redundant once the MCP SDK also exposes the ID token in the OauthToken model (already done in the TypeScript SDK), although I haven't looked into the full details of that.

@nbaju1 nbaju1 changed the title Azure (Entra) OAuth: add custom API audience with JWT verification, fix authorize params Azure (Entra) OAuth: Validate token against FastMCP app, not Graph. Sep 24, 2025
@JonasKs
Copy link

JonasKs commented Sep 24, 2025

Looks great! I'm not that involved in the protocol (especially DCR), so maybe you guys could fill me in: How does AI clients, e.g. Claude, Claude Code or even MCP inspector deal with refreshing of tokens? Requesting an offline_access scope would give the client a refresh token, but I suspect this is highly Azure Entra ID opinionated.

@jlowin
Copy link
Owner

jlowin commented Sep 25, 2025

@JonasKs refresh flows should be identical to normal OAuth flow; DCR only adjusts how clients are registered. The [dynamic] client receives a "normal" token + refresh token and from then on uses standard flows.

@JonasKs
Copy link

JonasKs commented Sep 25, 2025

@jlowin & @nbaju1 , then I believe this solution will still have flaws, and we need add a parameter requested_scopes (which can default to the client app reg), in order to support requesting refresh tokens (by appending offline_access as a requested scope I believe? This won’t return scp: offline_access as far as I know).

@nbaju1
Copy link
Contributor Author

nbaju1 commented Sep 26, 2025

@JonasKs we can add special handling for the Graph scopes. Add a new parameter required_graph_scopes: list[str] | None = None to AzureProvider which extends the prefixed_scopes list (un-prefixed of course) in the authorize method. Thoughts?

@JonasKs
Copy link

JonasKs commented Sep 28, 2025

Hmm..
From a devex perspective I think we should consider the API and how it can map to other providers.

I like:

  • upstream_scopes - what scopes to request from upstream IDP
  • validation_scopes(aka required_scopes) - which scopes to validate

I’ve mentioned this in my issue, but I think scope validation (and user role checks) should be on a per tool/resource/prompt/route basis, like in FastAPI with dependency injection.
That’s not the scope of this PR, but I think upstream_scopes(or a better name) would make sense if that was ever implemented.

@nbaju1
Copy link
Contributor Author

nbaju1 commented Sep 29, 2025

I'm not familiar with other IDPs, so if this is a pattern that is standard, i.e. requesting scopes from the IDP that isn't returned in the access token, I agree that the name should be more general.

I vote for getting the provider to work in this PR, then see about separating out the scope validation in a subsequent PR.

@JonasKs
Copy link

JonasKs commented Sep 29, 2025

I have no idea either to be honest. I'm on board with this!

@whitehat101
Copy link

I was able to get this branch working with my Azure tenant. I was initially getting token rejections: "Bearer token rejected for client" I enabled debug logs, and enhanced the JWTVerifier with more verbose debug logs, I was able to figure out what was wrong.

# src/fastmcp/server/auth/providers/jwt.py
            if self.issuer:
                if claims.get("iss") != self.issuer:
                    self.logger.debug(
                        "Token validation failed: issuer mismatch for client %s. Issuer expected: %s, Issuer received: %s",
                        client_id,
                        self.issuer,
                        claims.get("iss")
                    )
                    self.logger.info("Bearer token rejected for client %s.", client_id)
                    return None

It was complaining about the issuer:

Issuer expected: https://login.microsoftonline.com/TENANT-UUID/v2.0
Issuer received: https://sts.windows.net/TENANT-UUID/

I was getting a V1 token instead of a V2 token.

To fix this, I needed to edit my Azure App Registration's manifest to include:

{
	"accessTokenAcceptedVersion": 2
}

I read that this may be changing to requestedAccessTokenVersion, but my manifest wanted the "accepted" version.

@JonasKs
Copy link

JonasKs commented Sep 30, 2025

Yeah, this is Azure in a nut shell. The manifests has two versions, and which version you see can vary from app reg to app reg.

https://intility.github.io/fastapi-azure-auth/single-tenant/azure_setup#step-2---change-token-version-to-v2

@sakhan-rio
Copy link

@JonasKs I faced similar issue when using a different app reg. Can we also parametrize issuer in AzureProvider class?

Yeah, this is Azure in a nut shell. The manifests has two versions, and which version you see can vary from app reg to app reg.

https://intility.github.io/fastapi-azure-auth/single-tenant/azure_setup#step-2---change-token-version-to-v2

@JonasKs
Copy link

JonasKs commented Sep 30, 2025

I would strongly suggest not supporting v1 tokens, and rather document how to enforce v2. V1 is deperecated, and has a whole lot of extra problems to deal with. The code will end up with lots of if v1: else: .., and bugs will happen.
My code base for fastapi-azure-auth got so much simpler when I removed v1 tokens support.

@nbaju1
Copy link
Contributor Author

nbaju1 commented Sep 30, 2025

Updated the parameter name to additional_authorize_scopes (think this is a bit more clearer) and added the feature request in #1925. @jlowin ready for review from my end now.

@marrobi
Copy link

marrobi commented Oct 7, 2025

@JonasKs I faced similar issue when using a different app reg. Can we also parametrize issuer in AzureProvider class?

Yeah, this is Azure in a nut shell. The manifests has two versions, and which version you see can vary from app reg to app reg.
https://intility.github.io/fastapi-azure-auth/single-tenant/azure_setup#step-2---change-token-version-to-v2

@nbaju1 @JonasKs This is looking great. I've just been testing this PR as I'm doing some MCP work with Azure Health Data Services. The OAuth flow seems to be working up until querying my the AHDS API. It's giving back https://sts.windows.net/<tenant_id> as the issuer - which fails. I don't have control to change this.

https://learn.microsoft.com/en-us/azure/healthcare-apis/authentication-authorization#access-token

Maybe issuer does need to be configurable?

@JonasKs
Copy link

JonasKs commented Oct 7, 2025

Request v2 tokens, its described above. V1 is deprecated.

@marrobi
Copy link

marrobi commented Oct 7, 2025

Request v2 tokens, its described above. V1 is deprecated.

Thanks for the quick response. I might be trying to do something that's not supported, and don't want to derail the PR discussion, so if its not obvious adon't let me get in the way.

I'm using "on behalf of flow" ( https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-on-behalf-of-flow )

I have a client application, which as per the PR docs has:

"api": {
            "requestedAccessTokenVersion": 2
        }

This then has API permissions on a resource app (the Azure service) .

I have the resource app scopes configured under FASTMCP_SERVER_AUTH_AZURE_REQUIRED_SCOPES.

It's the resource app token that is coming back as the v1 token.

@marrobi
Copy link

marrobi commented Oct 7, 2025

@JonasKs ignore that, sorted it, I've set FASTMCP_SERVER_AUTH_AZURE_REQUIRED_SCOPES to blank, and using the user token to get the resource app token. For some reason (user error) FastMCP was trying to validate the resource token, it shouldn;t have been.

Be great to see this PR released. Thanks @nbaju1!

Copy link
Owner

@jlowin jlowin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you very much @nbaju1 and @JonasKs for working on this! This is a huge relief to see validated with such rigor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auth Related to authentication (Bearer, JWT, OAuth, WorkOS) for client or server. bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality.

Projects

None yet

6 participants